前面我們已經完成了 API 與 Redis 的串接,目前的架構大概是:
Client
↓
Service api
↓
FastAPI Pod
↓
Service redis
↓
Redis Pod
到這裡,我們已經實際碰到 Kubernetes 裡很重要的一個概念:
Service-to-Service Communication
API 不需要知道 Redis Pod 的 IP,只需要連線到:
redis:6379
Kubernetes Service 會幫我們找到真正的 Redis Pod。
今天,我們要再加入第三個元件:
PostgreSQL
最後整個架構會逐漸變成:
Client
↓
Service api
↓
FastAPI Pod
├── Service redis
│ ↓
│ Redis Pod
│
└── Service postgres
↓
PostgreSQL Pod
↓
Persistent Storage
不過 PostgreSQL 和前面的 API 有一個非常重要的差異。
API Pod 壞掉,其實沒有那麼可怕。
因為 Deployment 可以重新建立一顆新的 Pod:
API Pod A
被刪除
↓
Deployment 發現 replicas 不足
↓
建立 API Pod B
新的 Pod 啟動之後,Application 還是可以繼續提供服務。
PostgreSQL Pod 本身其實也一樣。
Pod 壞掉之後,也可以重新建立:
PostgreSQL Pod A
↓
刪除
↓
PostgreSQL Pod B
真正的問題不是:
Pod 能不能重建?
而是:
資料能不能留下來?
如果 PostgreSQL 裡面的 User、Order、Article、Transaction 等資料,跟著 Pod 一起消失,那就算 Kubernetes 可以在幾秒內建立新的 PostgreSQL Pod,也沒有任何意義。
因此今天真正要理解的主題,其實不是 PostgreSQL,而是:
Persistent Storage
也就是:
如何讓 Pod 可以被刪掉、重建、替換,但資料仍然存在。
先想像 PostgreSQL Container 裡面有這個目錄:
/var/lib/postgresql/data
這是 PostgreSQL 預設用來存放 Database Data 的重要位置之一。
如果我們完全沒有配置 Volume,那 PostgreSQL 寫入的資料,就會存在 Container 自己的 Filesystem 裡面。
概念上可以想成:
PostgreSQL Pod
└── Container
└── /var/lib/postgresql/data
├── users
├── tables
├── indexes
└── database files
問題就在這裡。
Container 的 Filesystem 是跟著 Container 生命週期走的。
假設今天 PostgreSQL Pod 被刪除:
kubectl delete pod postgres-xxxxx
流程會變成:
Pod 被刪除
↓
Container 被刪除
↓
Container Filesystem 消失
↓
PostgreSQL Data 消失
Deployment 確實會重新建立一顆 PostgreSQL Pod。
但是新的 PostgreSQL Container 使用的是一份新的 Filesystem。
所以結果可能變成:
PostgreSQL Pod A
Database 有資料
↓
Pod 被刪除
↓
PostgreSQL Pod B
Database 變成全新的
這顯然不是 Database 可以接受的行為。
因此我們需要把:
Application 的生命週期
和:
Data 的生命週期
分開。
我們希望做到:
Pod 可以消失
Container 可以消失
但 Storage 不要消失
這就是 Kubernetes Volume 與 Persistent Storage 要解決的問題。
Kubernetes 在處理 Persistent Storage 時,會看到兩個非常重要的 Resource:
PV
PVC
完整名稱分別是:
PV = PersistentVolume
PVC = PersistentVolumeClaim
第一次看到很容易搞混。
可以先用「租房子」理解。
假設市場上真的有一間房子:
房子
這可以理解成:
PersistentVolume
也就是:
真正可以使用的 Storage 資源。
例如底層可能來自:
Local Disk
AWS EBS
GCP Persistent Disk
Azure Disk
NFS
但 Application 通常不需要直接指定:
我要使用 PV-abc123
而是提出自己的需求。
例如:
我需要一個至少 1 GiB,而且可以 Read / Write 的 Storage。
這個「我要一個怎樣的 Storage」的要求,就是:
PersistentVolumeClaim
也就是 PVC。
所以可以理解成:
PV
=
真的存在的房子
PVC
=
我要租一間符合這些條件的房子
Pod
=
租客
最後關係會變成:
Pod
↓
PVC
↓
PV
↓
真正的 Storage
Pod 不需要直接管理底層 Disk。
Pod 只需要說:
我要使用 postgres-pvc
至於這個 PVC 最後對應哪個 Storage,交給 Kubernetes 的 Storage 機制處理。
這個抽象非常重要。
因為 Kubernetes 希望 Application 關心的是:
我要多少 Storage?
我要什麼存取模式?
而不是:
這顆 Disk 在哪?
Device ID 是多少?
AWS EBS Volume ID 是多少?
建立:
k8s/05-postgres-pvc.yaml
內容:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: cka-lab
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
先看最重要的部分:
resources:
requests:
storage: 1Gi
意思就是:
我希望 Kubernetes 幫我準備至少 1 GiB 的 Storage。
這不是建立一個 1 GiB 的 Folder,而是在向 Kubernetes 的 Storage System 提出:
Storage Request
接著是:
accessModes:
- ReadWriteOnce
這個地方非常容易被誤解。
ReadWriteOnce,縮寫:
RWO
比較準確的意思是:
這個 Volume 可以被一個 Node 以 Read/Write 模式掛載。
注意,是:
一個 Node
不是單純:
只能一個 Pod
如果多個 Pod 剛好位於同一個 Node,某些 Storage 情況下仍可能共同使用該 Volume。
因此目前先記成:
ReadWriteOnce
=
同一時間主要由一個 Node
以 Read / Write 方式掛載
Storage Access Mode 後面的 Storage 章節,我們會再完整拆解:
ReadWriteOnce
ReadOnlyMany
ReadWriteMany
ReadWriteOncePod
目前 PostgreSQL 單節點 Lab 使用 ReadWriteOnce 就可以了。
這時可能會有一個疑問:
我們明明只建立:
PVC
可是沒有建立:
PV
那 Storage 到底哪裡來?
如果你的 Kubernetes Cluster 有設定:
StorageClass
就可以透過:
Dynamic Provisioning
自動建立 Storage。
例如我們目前使用 kind 或其他本機 Kubernetes 環境時,通常環境本身會提供某種 Storage Provisioner。
因此可能發生:
建立 PVC
↓
StorageClass 發現新的 Storage Request
↓
自動 Provision Storage
↓
建立 / 配對 PV
↓
PVC 與 PV 綁定
這也是為什麼等等查看 PVC 時,我們希望看到:
Bound
意思就是:
這個 PVC 已經成功取得 Storage。
接下來建立:
k8s/06-postgres.yaml
內容:
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: cka-lab
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: app-secret
env:
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: cka-lab
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
這份 YAML 裡面真正的新東西主要是:
volumes
volumeMounts
persistentVolumeClaim
而這三個東西的關係一定要搞懂。
volumes 到底是在做什麼?先看 Pod 裡面的:
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
這裡是在告訴 Kubernetes:
這個 Pod 需要一個叫做
postgres-data的 Volume。
而這個 Volume 的來源不是 Container Filesystem,而是:
persistentVolumeClaim:
claimName: postgres-pvc
也就是我們剛才建立的:
postgres-pvc
所以可以理解成:
postgres-pvc
↓
postgres-data
postgres-data 是:
Pod 裡面對這個 Volume 使用的名稱
而真正的 Storage 來源是:
postgres-pvc
volumeMounts 又是什麼?接著 Container 裡有:
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
這是在告訴 Kubernetes:
把 Pod 裡叫做
postgres-data的 Volume,掛載到這個 Container 的/var/lib/postgresql/data。
因此:
volumes:
負責的是:
這個 Pod 有哪些 Volume?
Volume 從哪裡來?
而:
volumeMounts:
負責的是:
這個 Container
要把 Volume 掛在哪裡?
完整資料流可以畫成:
PersistentVolume
↑
│
PersistentVolumeClaim
postgres-pvc
↑
│
Pod Volume
postgres-data
↓
│
Container
/var/lib/postgresql/data
PostgreSQL 看起來還是在正常寫:
/var/lib/postgresql/data
但是這個 Path 現在已經不是單純存在 Container Filesystem 裡。
它背後連接到的是:
Persistent Storage
所以 PostgreSQL 寫入:
/var/lib/postgresql/data
實際上資料會進入:
PVC
↓
PV
↓
Storage
這就是 Persistence 的核心。
我們另外寫了:
env:
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
PostgreSQL 官方 Image 會透過:
PGDATA
決定實際 Database Cluster 的資料目錄。
因此目前結構會比較像:
/var/lib/postgresql/data
└── pgdata
├── base
├── global
├── pg_wal
└── ...
也就是:
PVC 掛載:
/var/lib/postgresql/data
PostgreSQL 真正 Data Directory:
/var/lib/postgresql/data/pgdata
對我們目前的 Lab 來說,先理解:
PostgreSQL 的資料目錄
位於 Persistent Volume 裡
就足夠了。
YAML 後半段還建立:
kind: Service
名稱:
metadata:
name: postgres
它會透過:
selector:
app: postgres
找到 PostgreSQL Pod。
所以之後 FastAPI 不需要知道 PostgreSQL Pod IP。
FastAPI 可以直接連:
postgres:5432
架構就變成:
FastAPI Pod
↓
postgres:5432
↓
Service postgres
↓
PostgreSQL Pod
這和前面的:
redis:6379
完全是相同概念。
所以到現在為止,我們其實已經把 Kubernetes 裡兩個非常重要的問題分開解決:
Service
→ 解決「我要怎麼找到另一個 Pod?」
PVC
→ 解決「Pod 消失後,資料怎麼留下?」
建立好 YAML 之後:
kubectl apply -f k8s/
接著先確認 Pod:
kubectl get pods -n cka-lab
應該可以看到 PostgreSQL Pod。
!
接著:
kubectl get pvc -n cka-lab
https://ithelp.ithome.com.tw/upload/images/20260910/201685376ZlCiyliFr.png
最重要的是:
STATUS = Bound
Bound 代表:
PVC 已經成功找到並綁定一個 PersistentVolume。
也就是:
PVC
↓
PV
已經建立關係。
如果看到:
Pending
則代表:
PVC 還沒有取得 Storage
這時才需要進一步檢查 StorageClass、Provisioner 或 PV。
接著執行:
kubectl get pv
可能會看到:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS
pvc-xxxxx 1Gi RWO Delete Bound

這就是 PVC 背後真正配對到的:
PersistentVolume
所以目前關係大概是:
PostgreSQL Pod
↓
postgres-data
↓
postgres-pvc
↓
PV
↓
Storage
到這裡雖然概念看起來合理,但光看到:
Bound
還不代表你真的理解 Persistence。
最好的方法是:
真的把 Pod 刪掉。
首先進入 PostgreSQL:
kubectl exec \
-it \
deployment/postgres \
-n cka-lab \
-- \
psql \
-U appuser \
-d appdb
這個指令可以理解成:
kubectl exec
↓
進入 deployment/postgres 的 Pod
↓
執行 psql
↓
User:appuser
↓
Database:appdb
進去之後建立一張測試 Table:
CREATE TABLE demo (
id SERIAL PRIMARY KEY,
message TEXT
);
再利用
\d demo
去查看建立好的欄位

接著插入一筆資料:
INSERT INTO demo (message)
VALUES ('PVC survives pod replacement');
查詢:
SELECT * FROM demo;
應該會看到類似:
id | message
----+---------------------------------
1 | PVC survives pod replacement

現在這筆資料真的已經寫進 PostgreSQL。
離開 PostgreSQL:
\q
執行:
kubectl delete pod \
-n cka-lab \
-l app=postgres
這個指令會把符合:
app=postgres
Label 的 Pod 刪除。
接著查看:
kubectl get pods -n cka-lab
你可能會看到舊 Pod 消失,然後一顆新的 PostgreSQL Pod 被建立。
原因是 Deployment 裡寫了:
replicas: 1
Deployment 的期待是:
永遠維持 1 個 PostgreSQL Pod。
所以:
你手動刪掉 Pod
↓
Deployment 發現
Current = 0
Desired = 1
↓
自動建立新的 Pod
例如:
原本:
postgres-7c8d9c7b56-aaaaa
刪除之後建立了新的:
postgres-7c8d9c7b56-bbbbb
Pod Name 已經不同。
這證明:
現在是新的 Pod。
等新的 Pod:
READY 1/1
STATUS Running
之後,再次進入 PostgreSQL:
kubectl exec \
-it \
deployment/postgres \
-n cka-lab \
-- \
psql \
-U appuser \
-d appdb
再次執行:
SELECT * FROM demo;
如果還看到:
id | message
----+---------------------------------
1 | PVC survives pod replacement

這時候其實就已經實際證明了一件非常重要的事情:
舊 PostgreSQL Pod
已經不存在。
新的 PostgreSQL Pod
已經建立。
但 Database Data
仍然存在。
原因就是資料並不是綁在:
Pod
而是存在:
Persistent Storage
新的 PostgreSQL Pod 啟動之後,再重新掛載同一個 PVC:
postgres-pvc
於是它重新看到原本的 Database Data。
整個過程就是:
PostgreSQL Pod A
↓
PVC
↓
Data
Pod A 被刪除
PostgreSQL Pod B
↓
同一個 PVC
↓
同一份 Data
這就是:
Persistence
完整的流程是:
你建立 PVC
↓
Kubernetes 看有沒有可以用的 PV
↓
如果沒有,而且 Cluster 有 StorageClass + Provisioner
↓
Provisioner 動態建立 Storage
↓
Kubernetes 建立對應的 PV
↓
PVC ↔ PV 綁定(Bound)
↓
Pod 使用 PVC
↓
把這個 Storage 掛進 Container
Kubernetes 官方把這叫做 Dynamic Provisioning;它依賴 StorageClass 與 provisioner。沒有這套機制、也沒有事先建立好的合適 PV 時,PVC 可能就會一直停在 Pending。
假設你寫:
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
resources:
requests:
storage: 1Gi
你其實只是在跟 Kubernetes 說:
「我要 1Gi 的硬碟空間。」
你還沒有叫 PostgreSQL 使用它。
建立 PVC 後,如果你的 Cluster 有預設 StorageClass,例如:
PVC: 我要 1Gi
↓
StorageClass: 好,我知道要找誰生硬碟
↓
Provisioner
↓
建立真正 Storage
↓
產生 PV
↓
PVC Bound 到 PV
StorageClass 本身就是描述「該用哪個 provisioner、什麼參數去配置 Storage」的資源。
然後第二件事才是你的 Deployment 說:
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
意思不是「建立 PV」。
而只是:
我的 Pod 要使用已經叫做
postgres-pvc的這份 Storage。
再來第三件事:
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
這是在說:
把剛才那份 Storage,掛到 PostgreSQL Container 的
/var/lib/postgresql/data。
所以你可以把整件事理解成:
① PVC:我要硬碟
postgres-pvc
「我要 1Gi」
② Storage System:準備硬碟
StorageClass
↓
Provisioner
↓
PV / 真正 Storage
③ Deployment:我要使用那顆硬碟
Pod
↓
postgres-pvc
↓
PV
④ volumeMounts:掛到 Container 哪裡
PostgreSQL Container
└── /var/lib/postgresql/data
↓
Persistent Storage
這才是最關鍵的。
PostgreSQL 自己根本不知道什麼叫:
PVC
PV
StorageClass
它只知道:
我要把資料寫到:
/var/lib/postgresql/data
原本沒有掛 PVC 時:
PostgreSQL
↓
/var/lib/postgresql/data
↓
Container Filesystem
↓
Pod Delete
↓
💥 Data 消失
現在你加了:
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
之後:
PostgreSQL
↓
/var/lib/postgresql/data
↓
PVC
↓
PV
↓
真正 Storage
所以 PostgreSQL 還是一模一樣:
寫 /var/lib/postgresql/data
但 Kubernetes 偷偷把這個資料夾「接到外面的 Storage」。
這就是 Volume Mount 最實際的意義。
今天最重要的不是背:
PV = PersistentVolume
PVC = PersistentVolumeClaim
真正需要理解的是 Kubernetes 把:
Compute
和:
Storage
分開管理。
Pod 屬於 Compute。
它本來就是:
Ephemeral 短暫的
也就是可以被:
建立
刪除
重新排程
替換
因此永遠不要假設:
某一顆 Pod 會一直存在。
但 Database Data 的需求完全相反。
我們希望:
Pod 可以換
Data 不要換
所以 Kubernetes 透過:
PVC
把 Pod 和 Persistent Storage 連接起來。
最後你可以把今天整個概念濃縮成:
Service
解決:
Pod 怎麼找到 Pod?
Deployment
解決:
Pod 掛掉之後誰把它建立回來?
PVC / PV
解決:
Pod 被換掉之後資料怎麼留下?
這三個概念開始組合起來之後,你的 Kubernetes 架構才真正開始像一個完整的 Application:
Client
↓
Service api
↓
FastAPI Pod
↙ ↘
↓ ↓
Service redis Service postgres
↓ ↓
Redis Pod PostgreSQL Pod
↓
PVC
↓
PV
↓
Persistent Storage
這也是今天最重要的一句話:
Pod 是可以被替換的運算單位;真正需要長期保存的資料,不應該依賴 Pod 的生命週期。
因此當你親手執行:
kubectl delete pod
看著 PostgreSQL Pod 被換掉,再執行:
SELECT * FROM demo;
發現資料依然存在時,你理解的就不再只是:
PVC = PersistentVolumeClaim
而是真的理解了 Kubernetes 裡的:
Persistence。
另外補充一個之後會遇到的重要觀念:這一章使用 Deployment + PostgreSQL + PVC 非常適合拿來學習 Volume 與 Persistence;但正式環境中的 Stateful Database,通常還會進一步學習 StatefulSet、StorageClass、Backup、Replication 等機制。
現階段先把「Pod 可以消失,但 Data 不應該跟著消失」這個核心觀念徹底搞懂即可!